This is a demo program which shows how the bootstrap code in the MBR generated by the MS DOS fdisk program works. It has been reimplemented as a COM executable so that it can be ran on its own but has been modified so that it won’t actually try to boot an operating system
Code
MBRDemo.asm
Memory Layout
COM applications are very simple, all of their code and data resides in a single memory segment and they are executed by simply loading the binary into memory and running it starting at 0x0100. This is useful for us because we can create an approximation of what memory would look like when a drive’s boot sector is loaded.
When a computer starts up it loads the boot sector of the first drive at 0x7C00 and then starts execution there. We mimic this by using the ORG 7C00H directive to put the bulk of the code at that address. This isn’t quite accurate as this is 0x7C00 in whatever segment DOS has loaded our program into and not 0000:7C00 like the ROM BIOS would use, but it’s good enough for our purposes. The only concern is that we can’t use absolute far jumps as we don’t know the selector value but there’s a workaround for that.
There are also a series of labels starting with the ORG 0600H directive, there isn’t any actual code put here but this is where there bootstrap loader will put a copy of itself. This makes it easier to specify offsets later
COM Stub
All COM applications are loaded at 0x100 as the Program Segment Prefix (PSP) precedes it. The program starts with the ORG 100H directive to indicate this. It then has an ASSUME statement which lets the assembler know that all of the segments will be the same.
The program starts by loading CS into AX. This is done so that AX will contain the common segment address when we move to the bootstrap code. This isn’t strictly necessary as the segment selectors will all be set correctly anyways but it allows us to keep more of the bootstrap code. After this the program jumps to the bootstrap code.
The second part of the stub after the finish: label uses the DOS system call to terminate the program and provides a return code of 0
Bootstrap Code Initialization
This code is all based on a disassembly of the bootstrap code that the fdisk /mbr operation writes to a disk.
The original code starts with the XOR AX, AX statement which is an easy way to set a register to 0 (When XORing values, the resulting bit is 0 if the bits from the two operands are the same and all of the bits in a value will be the same as themselves so XORing a value with itself always gives 0). This is commented out in the demo because we want to use the segment that was used to load the program and not 0.
The bootstrap code then sets the Stack Selector to AX and sets the Stack Pointer to 0x7C00. The stack on an x86 system grows downward so this will cause the area right under the bootstrap code to be used as stack space. The program continues by copying SP to SI. It then pushes AX onto the stack and then pops ES and then repeats the operation for DS. This sets the two data segment selectors to AX. I’m not sure why it doesn’t also use MOV statements to set ES and DS as this seems to be allowed and looks like it would take less cycles.
Bootstrap Copy
Next the program makes a copy of the boot sector. This is done so that the partition boot sector can be loaded at 0x7C00 (Where the disk boot sector currently is) and all boot code can be written to expect that same address. It starts by clearing the direction flag. It then puts the address of where the boot sector should be copied to in DI and the number of words to copy in CX. The demo program uses the REP MOVSW statement to copy multiple words of memory. The MOVSW instruction copies the value pointed to by DS:SI to ES:DI (this is why SP was copied to SI earlier) and then updates SI and DI. MOVSW increments the index registers if the direction flag is cleared and decrements them if it’s set. The REP prefix causes CX to be decremented after the instruction and for the instruction to be repeated until CX is 0. The original code used REPNE which repeats until the zero flag is set but since MOVSW doesn’t update the zero flag, this doesn’t make a difference
After the code is copied the program jumps to the copy. The original code used an absolute far jump at this point but since we don’t actually know our segment address, we have to load the offset into AX and then jump to AX. Thankfully those two instructions take up the same number of bytes as the absolute far jump.
Partition Test
Now in the copy, the code starts testing the partition entries. It loads the address of the first partition entry into SI and the number of partition entries into BL. It starts by comparing the first byte of the partition entry to 0x80. This first byte is the status byte and 0x80 indicates that this partition is marked active. When an active partition is found it jumps to the second part of the partition validation process. If the partition isn’t active, it then compares the status against 0x00. This is the only other valid status value and indicates that the partition is not active. If a different value is found then it jumps down to print the invalid partition table message, otherwise it moves SI to the next partition entry and decrements BL. If BL is not 0 it jumps back up to repeat the test. If BL is 0 then no active partition was found and the boot process is finished. In the original code it calls the INT 18H BIOS function which tries to load BASIC, in the demo it just jumps to display the invalid partition table message.
After finding an active partition the program stores the first two bytes of the partition entry in DX and the second two bytes in CX. These bytes contain the status byte the the three bytes that make up the starting location of the partition in CHS (Cylinder-Head-Sector) format. It then continues checking the other partitions similar to before. If another partition is also marked as active or a partition has an invalid status value, it displays the invalid invalid partition table message. If all of the partitions are valid it jumps down to try loading the partition’s boot sector.
Print Message
The print message logic displays a null terminated string stored in SI. The instruction to set the invalid partition message location is right above the logic for printing the message but for other messages the value gets loaded before jumping back to the message code.
The code uses the LODSB instruction to load a byte pointed at by SI into AL. It then compares AL to 0x00 to see if it’s the null terminator. If the value is 0 then the code jumps to the end of the print message logic. In the original code this would be a jump instruction that loops to itself causing the computer to spin. In the demo it’s just a jump to the end of the bootstrap code which returns execution to the COM stub
If the value is not 0, the program pushes SI onto the stack so that it can be retrieved later and then displays the character using INT 10H with AH set to 0x0E which is the BIOS system function to print a single character to the screen. Before this BX is set to 0x0007 which sets it to display on video page 0 with white text.
The program then restores SI and jumps back up to print the next character
Load Partition Boot sector
If one and only one active partition is found and the rest of the partition statuses are 0x00, then the program tries to load the boot sector for that active partition. It starts by setting DI to 0x5, this is used to keep track of the number of retry attempts. It then sets BX to 0x7C00 which indicates where data should be written to. It then loads AX with 0x0201, the 02 indicates that this is a read from disk operation and the 01 is the number of sectors to read. It pushes DI onto the stack so that it can be recovered later and then It uses the INT 13H instruction to BIOS function. This function gets the drive number from DL and the Cylinder, head and sector address from DH and CX. Conveniently the drive number for the first hard disk is 0x80 which is the status byte indicating that a partition is active and the format the function uses to get the CHS address is the same as the partition entry table uses. So earlier when the program loaded the first four bytes of the partition table entry into CX and DX it was actually preparing to call this function
After calling the read disk function the program restores DI. If the carry flag is clear it jumps down to start booting the partition. The read disk BIOs function sets the carry flag to indicate an error. If carry is set the program sets AX equal to 0x00 by XORing it with itself and then calls INT 13H again to trigger the reset disk system BIOS function. It then decrements DI. If DI is not zero then it jumps back up to try reading the partition again, otherwise it loads the error loading operating system message and jumps to the print message code
Boot Partition
If the partition sector is properly loaded the program then tests to see if this is a valid boot Sector. It starts by loading the missing operating system message address into SI and then loading the address of the last two bytes of the sector into DI. It then compares these two bytes against 0xAA55. This is a signature which indicates that this is a bootable partition. If the signature is not found the program jumps up to the print message code to print the message that was already loaded. Otherwise is moves BP into SI (Restoring the location of the partition entry for this partition).
In the original code the program would then jump to the partition boot sector and start booting the operating system. In the demo the program simply returns to the COM stub to terminate.
Boot Messages
After the end of the bootstrap code are the three messages that can be returned. Invalid partition table, error loading operating system and missing operating system
Partition Table
At the end of the boot sector are the four partition table entries. Each one consists of a status byte, three bytes that contain the starting CHS address, a byte containing the partition type, 3 bytes containing the ending CHS address, 4 bytes containing the starting LBA (Logical Block Address) and 4 bytes containing the length in number of sectors
Output
What the program outputs will depend on what’s in the partition entries
If the status of all partition entries is set to 0x00 then it will print Invalid partition table, although in the original code it would try to load BASIC at this point
If multiple partitions have a status of active (0x80) or if one partition has a value other than 0x80 or 0x00 then it will also show Invalid partition table
If one partition is active and the rest are inactive but it can’t load the sector at the location specified by the starting CHS address after 5 attempts, it will display Error loading operating system. This might happen if the CHS address is too large for the disk or doesn’t make sense with the number of cylinders, heads or sectors returned by the disk
If one partition is active and the sector can be loaded but it doesn’t end with 0xAA55 then Missing operating system will be displayed. This can happen if the CHS address doesn’t point to the start of a partition
If one partition is active and the sector is loaded and has the proper signature, the program displays nothing. In the original code it would attempt to boot the partition at this point